Théo GUIGUE & Élodie GAYRAUD

Calcul stochastique, Master 2 ARB

L'objectif principal de ce projet est d'utiliser quelques méthodes de pricing d'option afin de comprendre au mieux les différents paramètres déterminants sur les marchés financiers des options et de comprendre leurs rôles.

Dans ce projet, nous calibrons le modèle de Heston sur les données d’options américaines de TESLA nous tracons une nappe de volatilité puis nous calibrons un modèle de Dupires. Cela consiste à ajuster les paramètres du modèle pour qu’il reproduise le mieux possible les prix des options observés sur le marché. Cette calibration permettra d'évaluer la performance du modèle dans la valorisation des options et dans l'estimation de la volatilité implicite pour les différentes maturités et prix d'exercice (strikes). Le modèle de Heston est un modèle de volatilité stochastique utilisé pour valoriser les options financières. Contrairement au modèle de Black-Scholes, où la volatilité est constante, le modèle de Heston suppose que la volatilité évolue de manière stochastique, ce qui lui permet de capturer des phénomènes observés sur le marché, tels que le smile de volatilité.

Calibration d'un modèle de Heston et nappe de volatilité par Black-Sholes¶

Cadre théorique du modèle de Heston¶

Modèle de volatilité stochastique de Heston sous la mesure de probabilité réelle¶

$\large dS_t = \mu S_t \, dt + \sqrt{v_t} S_t \, dW^\mathbb{P}_{1,t}$

$\large dv_t = \kappa (\theta - v_t) \, dt + \sigma \sqrt{v_t} \, dW^\mathbb{P}_{2,t}$

$\large \rho \, dt = dW^\mathbb{P}_{1,t} \, dW^\mathbb{P}_{2,t}$

  • Le modèle de Heston repose sur deux équations différentielles stochastiques (EDS) couplées :
    • La première décrit la dynamique du prix de l'actif sous-jacent $S_t$, qui suit un mouvement brownien géométrique avec une volatilité instantanée $\sqrt{v_t}$.
    • La seconde décrit l'évolution de la variance $v_t$ comme un processus d'Ornstein-Uhlenbeck à reflet positif. Ce processus tend à revenir à une moyenne de long terme $\theta$ avec un taux de reversion $\kappa$. La volatilité de la variance est quantifiée par $\sigma$.
  • Les deux processus sont corrélés par un coefficient $\rho$, ce qui permet de modéliser l'interdépendance entre le prix de l'actif et sa variance.

Utilisation du théorème de Girsanov pour passer de $\mathbb{P}$ à $\mathbb{Q}$¶

$\large dW^\mathbb{Q}_{S,t} = dW^\mathbb{P}_{S,t} + \alpha_S \, dt, \quad \alpha_S = \frac{\mu_\mathbb{P} - r}{\sqrt{v_t}}$

$\large dW^\mathbb{Q}_{v,t} = dW^\mathbb{P}_{v,t} + \alpha_v \, dt, \quad \alpha_v = \frac{\lambda}{\sigma^\mathbb{P}} \sqrt{v_t}$

  • Le théorème de Girsanov permet de changer la mesure de probabilité d'un modèle stochastique pour passer de la mesure réelle $\mathbb{P}$ (reflétant l'évolution naturelle du marché) à la mesure neutre au risque $\mathbb{Q}$ (reflétant une valorisation financière où tous les actifs offrent le même rendement ajusté au risque).
  • Le passage de $\mathbb{P}$ à $\mathbb{Q}$ ajuste les marches aléatoires $dW^\mathbb{P}$ par un terme de dérive $\alpha_S$ ou $\alpha_v$, qui intègre les primes de risque :
    • $\alpha_S$ dépend de l'écart entre le rendement réel $\mu_\mathbb{P}$ et le taux sans risque $r$.
    • $\alpha_v$ dépend de la prime de risque de variance $\lambda$, reflétant la compensation demandée par les investisseurs pour supporter l'incertitude liée à la volatilité.

Modèle de volatilité stochastique de Heston sous la mesure neutre au risque¶

$\large dS_t = r S_t \, dt + \sqrt{v_t} S_t \, dW^\mathbb{Q}_{1,t}$

$\large dv_t = \kappa^\mathbb{Q} (\theta^\mathbb{Q} - v_t) \, dt + \sigma \sqrt{v_t} \, dW^\mathbb{Q}_{2,t}$

$\large \rho^\mathbb{Q} \, dt = dW^\mathbb{Q}_{1,t} \, dW^\mathbb{Q}_{2,t}$

  • Sous la mesure neutre au risque $\mathbb{Q}$, l'évolution de $S_t$ est ajustée pour inclure uniquement le taux sans risque $r$, conformément à l'hypothèse d'absence d'arbitrage.
  • La dynamique de la variance $v_t$ est également modifiée, les paramètres étant ajustés pour intégrer la prime de risque de variance :
    • $\kappa^\mathbb{Q} = \kappa + \lambda$ : Taux de reversion augmenté pour tenir compte de la compensation demandée pour le risque de variance.
    • $\theta^\mathbb{Q} = \frac{\kappa \theta}{\kappa + \lambda}$ : Nouvelle moyenne de long terme ajustée à la prime de risque.

Paramètres spécifiques¶

  • $\lambda$ : Prime de risque de variance. Représente la compensation requise par les investisseurs pour porter le risque de fluctuations de la volatilité.
  • $\rho^\mathbb{Q} = \rho$ : Le paramètre de corrélation entre le prix de l’actif et la variance reste inchangé lors du changement de mesure.
  • $\kappa^\mathbb{Q} = \kappa + \lambda$ : Le taux de reversion augmente sous $\mathbb{Q}$, car il inclut une compensation pour le risque.
  • $\theta^\mathbb{Q} = \frac{\kappa \theta}{\kappa + \lambda}$ : La moyenne de long terme de la variance est ajustée pour refléter la nouvelle mesure.

Notations¶

  • $S_t$ : Prix spot de l'actif sous-jacent ou indice financier.
  • $v_t$ : Variance instantanée.
  • $C$ : Prix de l'option d'achat européenne.
  • $K$ : Prix d'exercice de l'option.
  • $W_{1,t}$, $W_{2,t}$ : Processus Brownien standards (associés respectivement à (S_t) et (v_t)).
  • $r$ : Taux d'intérêt sans risque.
  • $\kappa$ : Taux de retour à la moyenne sous (\mathbb{P}).
  • $\theta$ : Variance de long terme sous (\mathbb{P}).
  • $v_0$ : Variance initiale.
  • $\sigma$ : Volatilité de la variance.
  • $\rho$ : Corrélation entre les marches aléatoires de (S_t) et (v_t).
  • $t$ : Date courante.
  • $T$ : Date d’échéance.

En somme, le modèle de Heston est un cadre de calcul stochastique pour modéliser la volatilité des prix des actifs, tenant compte des caractéristiques réalistes telles que la variance stochastique et la corrélation avec les rendements. Il permet :

  1. Sous $\mathbb{P}$ : De décrire l'évolution naturelle des prix sur le marché.
  2. Sous $\mathbb{Q}$ : D'ajuster les paramètres pour intégrer les primes de risque et permettre la valorisation des options en cohérence avec l'absence d'arbitrage.

Ce modèle est particulièrement utile pour comprendre les sourires de volatilité et pour calibrer des stratégies de couverture plus réalistes.

Préparation des fonctions et données sous Python¶

In [1]:
import sys
!{sys.executable} -m pip install nelson_siegel_svensson

import yfinance as yf
import numpy as np
import pandas as pd
import math
from scipy.interpolate import interp1d, griddata
from scipy.integrate import quad
from scipy.ndimage import gaussian_filter
from scipy.optimize import minimize, brentq
from scipy.stats import norm
import matplotlib.pyplot as plt
from matplotlib import cm
import plotly.graph_objects as go
from plotly.graph_objs import Surface
from plotly.offline import iplot, init_notebook_mode
import plotly.io as pio
from datetime import datetime as dt
from nelson_siegel_svensson import NelsonSiegelSvenssonCurve
from nelson_siegel_svensson.calibrate import calibrate_nss_ols
from tabulate import tabulate
import warnings
from nbconvert.nbconvertapp import main

warnings.simplefilter(action='ignore', category=FutureWarning)
Requirement already satisfied: nelson_siegel_svensson in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (0.5.0)
Requirement already satisfied: Click>=8.0 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from nelson_siegel_svensson) (8.1.7)
Requirement already satisfied: numpy>=1.22 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from nelson_siegel_svensson) (2.1.2)
Requirement already satisfied: scipy>=1.7 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from nelson_siegel_svensson) (1.14.1)
Requirement already satisfied: matplotlib>=3.5 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from nelson_siegel_svensson) (3.9.2)
Requirement already satisfied: contourpy>=1.0.1 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (1.3.0)
Requirement already satisfied: cycler>=0.10 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (0.12.1)
Requirement already satisfied: fonttools>=4.22.0 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (4.54.1)
Requirement already satisfied: kiwisolver>=1.3.1 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (1.4.7)
Requirement already satisfied: packaging>=20.0 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (24.1)
Requirement already satisfied: pillow>=8 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (10.4.0)
Requirement already satisfied: pyparsing>=2.3.1 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (3.2.0)
Requirement already satisfied: python-dateutil>=2.7 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from matplotlib>=3.5->nelson_siegel_svensson) (2.9.0.post0)
Requirement already satisfied: six>=1.5 in /Library/Frameworks/Python.framework/Versions/3.10/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib>=3.5->nelson_siegel_svensson) (1.16.0)

Implémentation dans Python du modèle de Heston¶

Tout d'abord, la fonction caractésitique est implémentée :

$\large \varphi(X_0, K, v_0,\tau; \phi) = e^{r \phi i \tau} S^{i \phi}[\frac{1-ge^{d\tau}}{1-g}]^{\frac{-2a}{\sigma^2}} exp[\frac{a \tau}{\sigma^2} (b_2 -\rho\sigma \phi i + d) + \frac{v_0}{\sigma^2}(b_2 -\rho\sigma \phi i + d)[\frac{1-e^{d\tau}}{1-ge^{d\tau}}]]$

où d and g ne change pas en fonction de b1, b2 or u1, u2

  • $\large d = \sqrt{(\rho\sigma \phi i - b)^2 + \sigma^2 (\phi i + \phi^2)}$
  • $\large g = \frac{b -\rho\sigma \phi i + d}{b -\rho\sigma \phi i - d}$
  • $\large a = \kappa \theta$
  • $\large b = \kappa + \lambda$
In [2]:
def heston_charfunc(phi, S0, v0, kappa, theta, sigma, rho, lambd, tau, r):
    a = kappa * theta
    b = kappa + lambd
    rspi = rho * sigma * phi * 1j
    d = np.sqrt((rho * sigma * phi * 1j - b)**2 + (phi * 1j + phi**2) * sigma**2)
    g = (b - rspi + d) / (b - rspi - d)
    exp1 = np.exp(r * phi * 1j * tau)
    term2 = S0**(phi * 1j) * ((1 - g * np.exp(d * tau)) / (1 - g))**(-2 * a / sigma**2)
    exp_value = a * tau * (b - rspi + d) / sigma**2 + v0 * (b - rspi + d) * ((1 - np.exp(d * tau)) / (1 - g * np.exp(d * tau))) / sigma**2
    exp_value = np.clip(exp_value, -700, 700)
    exp2 = np.exp(exp_value)
    return exp1 * term2 * exp2

Ensuite, l'intégrale est définie comme une fonction dans Python :

$\large \int^{\infty}_0 \Re [ e^{r\tau} \frac{\varphi(\phi-i)}{i\phi K^{i\phi}} - K\frac{\varphi(\phi)}{i\phi K^{i\phi}} ] d\phi$

In [3]:
def integrand(phi, S0, v0, kappa, theta, sigma, rho, lambd, tau, r):
    args = (S0, v0, kappa, theta, sigma, rho, lambd, tau, r)
    numerator = np.exp(r * tau) * heston_charfunc(phi - 1j, *args) - K * heston_charfunc(phi, *args)
    denominator = 1j * phi * K**(1j * phi)
    return numerator / denominator

Effectuer une intégration numérique sur l'intégrande et calculer le prix de l'option.

$\large C(S_0, K, v_0, \tau) = \frac{1}{2}(S_0 - Ke^{-r \tau}) + \frac{1}{\pi} \int^{\infty}_0 \Re [ e^{r\tau} \frac{\varphi(\phi-i)}{i\phi K^{i\phi}} - K\frac{\varphi(\phi)}{i\phi K^{i\phi}} ] d\phi$

Pour cela, une intégration rectangulaire est utilisée :

In [4]:
def heston_price_rec(S0, K, v0, kappa, theta, sigma, rho, lambd, tau, r):
    args = (S0, v0, kappa, theta, sigma, rho, lambd, tau, r)
    P, umax, N = 0, 100, 10000
    dphi = umax / N
    for i in range(1, N):
        phi = dphi * (2 * i + 1) / 2
        numerator = np.exp(r * tau) * heston_charfunc(phi - 1j, *args) - K * heston_charfunc(phi, *args)
        denominator = 1j * phi * K**(1j * phi)
        P += dphi * numerator / denominator
    return np.real((S0 - K * np.exp(-r * tau)) / 2 + P / np.pi)

Implémentation d'une courbe des taux par un modèle NSS¶

Le code suivant récupère les taux d'intérêt américains pour différentes maturités depuis Yahoo Finance via une API, les transforme en un format compatible avec le modèle NSS, et utilise ce modèle pour calibrer une courbe des taux d'intérêt ajustée aux données observées.

Focus sur le modèle NSS :

Le modèle NSS sert à modéliser la structure par terme des taux d'intérêt (relation entre les taux d'intérêt et les maturités) de manière flexible tout en minimisant le nombre de paramètres nécessaires.\

Dans sa formulation mathématique. La courbe des taux $y(\tau)$ est donnée par l'équation suivante :

$ y(\tau) = \beta_0 + \beta_1 \frac{1 - e^{-\lambda \tau}}{\lambda \tau} + \beta_2 \left( \frac{1 - e^{-\lambda \tau}}{\lambda \tau} - e^{-\lambda \tau} \right) $

  • $\beta_0$ : Niveau de la courbe des taux (composante constante).
    Représente le taux à long terme, lorsque la maturité (( \tau )) tend vers l'infini.

  • $ \beta_1 $ : Pente de la courbe des taux.
    Reflète la différence entre les taux courts et les taux longs.

  • $ \beta_2 $ : Courbure de la courbe.
    Modélise la convexité de la courbe des taux, influençant la partie médiane des maturités.

  • $ \lambda $ : Paramètre d'échelle.
    Contrôle la position de la courbure maximale sur la courbe des taux.

  • $ \tau $ : Maturité (en années).

  1. Composante constante ($ \beta_0 $) :
    Modélise le niveau global des taux d'intérêt.
    Agit de manière uniforme sur toutes les maturités.

  2. Composante de pente ($ \beta_1 $) :
    Modélise la différence entre les taux courts et longs (pente de la courbe).
    Sa contribution diminue avec la maturité.

  3. Composante de courbure ($ \beta_2 $) :
    Affecte principalement les maturités intermédiaires.
    La contribution est maximale lorsque $ \tau \approx \frac{1}{\lambda} $.

In [5]:
tickers = ["^IRX", "^FVX", "^TNX", "^TYX"]  # Indices pour 3 mois, 5 ans, 10 ans, 30 ans par exemple
yield_data = []
for ticker in tickers:
    data = yf.Ticker(ticker)
    yield_data.append(data.history(period="1d")['Close'].iloc[-1])

# Convertissez les résultats en un format utilisable pour votre calibration
yield_maturities = np.array([0.25, 5, 10, 30])  # Maturités en années
yields = np.array(yield_data) / 100

curve_fit, status = calibrate_nss_ols(yield_maturities, yields)
curve_fit
Out[5]:
NelsonSiegelSvenssonCurve(beta0=np.float64(0.045792014737837644), beta1=np.float64(-0.001192361408445073), beta2=np.float64(-0.009179205553183847), beta3=np.float64(-0.003850285481544756), tau1=np.float64(2.0), tau2=np.float64(5.0))

Surface et nappe de volatilité par le modèle de Black Scholes¶

Le modèle de Black-Scholes est un modèle mathématique couramment utilisé en finance pour évaluer le prix des options, en particulier les options européennes. Il a été développé par Fischer Black, Myron Scholes et Robert Merton dans les années 1970. Le modèle de Black-Scholes repose sur l'hypothèse que les prix des actifs financiers suivent un mouvement brownien géométrique, c'est-à-dire qu'ils évoluent de manière aléatoire et continue. Il prend en compte des paramètres tels que le prix de l'actif sous-jacent (S0), la volatilité du marché (Sigma), le temps restant jusqu'à l'expiration de l'option (T, ici en jour), et le taux d'intérêt sans risque (r, pour ici on utilisera l'Euribor 1 mois = 3.87%). En utilisant ces paramètres, le modèle de Black-Scholes calcule le prix théorique de l'option, ce qui permet aux investisseurs de prendre des décisions éclairées en matière d'achat ou de vente d'options.

Dans cette partie de code, nous calculons et organisons les volatilités implicites pour les options CALL sur l'action Tesla (TSLA) en fonction des différentes maturités et prix d'exercice (strikes). Ces informations sont ensuite structurées sous forme d'une surface de volatilité implicite.

Définition des fonctions nécessaires¶

Cadre théorique :

La volatilité implicite est calculée en inversant le prix d'une option donné par le modèle de Black-Scholes afin de retrouver la volatilité correspondant au prix observé sur le marché. Cela implique la résolution de l'équation suivante :

$ C_{\text{marché}} = C_{\text{modèle}}(S, K, T, r, \sigma) $

où :

  • $C_{\text{marché}}$ est le prix de l'option observé sur le marché,
  • $C_{\text{modèle}}(S, K, T, r, \sigma)$ est le prix théorique de l'option calculé par le modèle de Black-Scholes, donné par :

$ C_{\text{modèle}} = S N(d_1) - K e^{-rT} N(d_2), $

avec : $ d_1 = \frac{\ln(S / K) + (r + \sigma^2 / 2) T}{\sigma \sqrt{T}}, \quad d_2 = d_1 - \sigma \sqrt{T}, $

et $N(\cdot)$ représente la fonction de répartition de la loi normale standard.

La volatilité implicite, $\sigma_{\text{imp}}$, est obtenue en résolvant numériquement l'équation non linéaire :

$ C_{\text{marché}} - C_{\text{modèle}}(S, K, T, r, \sigma) = 0. $

Implémentation python :

  • black_scholes_call : Cette fonction implémente le modèle de Black-Scholes pour calculer le prix théorique d'une option CALL. Les paramètres pris en compte sont :

    • $S$ : Prix actuel de l'action.
    • $K$ : Prix d'exercice de l'option.
    • $T$ : Temps restant avant expiration (en années).
    • $r$ : Taux d'intérêt sans risque.
    • $\sigma$ : Volatilité de l'actif.
  • implied_volatility_call : Cette fonction calcule la volatilité implicite.
    Elle utilise la méthode de Brent (via scipy.optimize.brentq) pour trouver numériquement la valeur de $\sigma$ qui rend le prix théorique de Black-Scholes égal au prix observé du marché.

In [6]:
def black_scholes_call(S, K, T, r, sigma):
    d1 = (np.log(S / K) + (r + sigma**2 / 2) * T) / (sigma * np.sqrt(T))
    d2 = d1 - sigma * np.sqrt(T)
    return S * norm.cdf(d1) - K * np.exp(-r * T) * norm.cdf(d2)

def implied_volatility_call(S, K, T, r, market_price):
    # Définition d'une fonction de pricing à utiliser dans l'optimisation
    def objective_function(sigma):
        return black_scholes_call(S, K, T, r, sigma) - market_price
    try:
        return brentq(objective_function, 1e-6, 1)
    except ValueError:
        return np.nan

Récupération des données de marché¶

  • Prix actuel de l'action Tesla ($S$) : Utilisation de l'API Yahoo Finance pour obtenir le prix de clôture le plus récent.

  • Taux sans risque ($r$) : Le rendement de l'obligation américaine à 10 ans (^TNX) est récupéré automatiquement et converti en pourcentage.

  • Dates d'expiration des options : Toutes les dates d'expiration disponibles pour les options Tesla sont récupérées.

Calcul des volatilités implicites¶

Pour chaque date d'expiration :

  • Les données des options CALL (prix d'exercice, bid, ask) sont extraites.
  • Le temps restant avant expiration ($T$) est calculé en années.
  • La volatilité implicite est calculée pour chaque prix d'exercice en utilisant le prix moyen de l'option $\frac{\text{bid} + \text{ask}}{2}$.

Les résultats sont stockés dans un dictionnaire iv_surface contenant :

  • strike : Les prix d'exercice des options.
  • iv : Les volatilités implicites correspondantes.
In [7]:
# Ticker
ticker = 'TSLA'
stock = yf.Ticker(ticker)

# Récupération de toutes les dates d'expiration des options
expirations = stock.options

# Récupération du prix actuel de l'action
S = stock.history(period='1d')['Close'].iloc[-1]

# Récupération du taux de rendement de l'obligation américaine à 10 ans (symbole "^TNX")
tnx = yf.Ticker("^TNX")
tnx_data = tnx.history(period="1d")
r = tnx_data['Close'].iloc[0] / 100  # Utilisation de .iloc pour accéder à la première ligne

print(f"Taux de rendement de l'obligation américaine à 10 ans récupéré automatiquement : {r * 100}%")

# Dictionnaire pour stocker les volatilities implicites
iv_surface = {}

# Boucle sur chaque expiration pour récupérer les CALL options
for expiry in expirations:
    options = stock.option_chain(expiry)
    calls = options.calls
    T = (dt.strptime(expiry, '%Y-%m-%d') - dt.today()).days / 365.25  # Calcul de la maturité en années
    
    # Calcul des volatilities implicites pour chaque strike
    ivs = [implied_volatility_call(S, K, T, r, (bid + ask) / 2) 
           for K, bid, ask in zip(calls['strike'], calls['bid'], calls['ask'])]
    
    iv_surface[expiry] = {
        'strike': calls['strike'],
        'iv': ivs
    }
Taux de rendement de l'obligation américaine à 10 ans récupéré automatiquement : 4.265000343322754%

Il est aussi possible de tracer la surface des prix de l'option TESLA afin de se donner une première idée de l'option avant de filtrer.

In [28]:
# Récupération de toutes les dates d'expiration des options
expirations = stock.options

# Initialisation d'un dictionnaire pour stocker les prix moyens (bid + ask) / 2
price_surface = {}

# Boucle sur chaque expiration pour récupérer les CALL options
for expiry in expirations:
    options = stock.option_chain(expiry)
    calls = options.calls
    T = (dt.strptime(expiry, '%Y-%m-%d') - dt.today()).days / 365.25  # Calcul de la maturité en années
    
    # Calcul des prix moyens pour chaque strike
    prices = [(bid + ask) / 2 for bid, ask in zip(calls['bid'], calls['ask'])]
    
    price_surface[expiry] = {
        'strike': calls['strike'],
        'price': prices
    }

# Extraction des strikes communs
all_strikes = [v['strike'] for v in price_surface.values()]
common_strikes = set.intersection(*map(set, all_strikes))
common_strikes = sorted(common_strikes)

# Organisation des données pour construire la surface
maturities = []
prices = []

for expiry, data in price_surface.items():
    maturity = (dt.strptime(expiry, '%Y-%m-%d') - dt.today()).days / 365.25
    prices_filtered = [data['price'][i] for i, strike in enumerate(data['strike']) if strike in common_strikes]
    maturities.append(maturity)
    prices.append(prices_filtered)

# Conversion des données en DataFrame
price_arr = np.array(prices, dtype=object)
price_surface_df = pd.DataFrame(price_arr, index=maturities, columns=common_strikes)

# Préparation des données pour le graphique 3D
X, Y = np.meshgrid(price_surface_df.columns, price_surface_df.index)
Z = np.array(price_surface_df, dtype=float)

# Création de la surface avec Plotly
fig = go.Figure(data=[go.Surface(
    x=X, 
    y=Y, 
    z=Z,
    colorscale='Viridis',
    colorbar=dict(title='Option Price')
)])

# Mise en forme du graphique
fig.update_layout(
    title='Surface des prix d\'options (Bid+Ask)/2 pour TSLA',
    scene=dict(
        xaxis_title='Strike',
        yaxis_title='Maturity (Years)',
        zaxis_title='Price'
    ),
    height=800,
    width=800
)

# Affichage du graphique
pio.show(fig)

Filtrage des données¶

  • Les strikes communs entre toutes les dates d'expiration sont identifiés et conservés.
  • Les maturités ($T$) sont filtrées pour ne garder que celles comprises entre $0.04$ ans ($\approx 15$ jours) et $1$ an.
  • Les prix d'exercice (strikes) sont limités à la plage $[250, 500]$ pour ne pas avoir trop de bruitage.

Organisation des résultats¶

Les volatilités implicites sont réorganisées :

  • Les maturités sont utilisées comme index.
  • Les prix d'exercice (strikes) comme colonnes.
  • Les données sont converties en un DataFrame nommé vol_surface.
In [25]:
# Récupération des strikes communs
all_strikes = [v['strike'] for v in iv_surface.values()]
common_strikes = set.intersection(*map(set, all_strikes))
common_strikes = sorted(common_strikes)

# Organisation des données
iv_data = []
maturities = []
for date, v in iv_surface.items():
    maturity = (dt.strptime(date, '%Y-%m-%d') - dt.today()).days / 365.25
    iv = [v['iv'][i] for i, x in enumerate(v['strike']) if x in common_strikes]
    iv_data.append(iv)
    maturities.append(maturity)

# Conversion en DataFrame
iv_arr = np.array(iv_data, dtype=object)
vol_surface = pd.DataFrame(iv_arr, index=maturities, columns=common_strikes)
vol_surface = vol_surface.loc[
    (vol_surface.index > 0.04) & (vol_surface.index < 1),
    (vol_surface.columns > 220) & (vol_surface.columns < 450)
]

vol_surface
Out[25]:
230.0 240.0 250.0 260.0 270.0 280.0 290.0 300.0 310.0 320.0 ... 350.0 360.0 370.0 380.0 390.0 400.0 410.0 420.0 430.0 440.0
0.043806 0.942965 0.925472 0.833413 0.819795 0.725372 0.69591 0.671192 0.640373 0.618982 0.596616 ... 0.628817 0.638405 0.647488 0.650904 0.672556 0.683658 0.701624 0.71657 0.739181 0.757239
0.062971 0.917574 0.855909 0.837082 0.723814 0.691978 0.649488 0.651683 0.611813 0.603686 0.60626 ... 0.612128 0.618602 0.625265 0.635299 0.645248 0.655265 0.666306 0.679221 0.692904 0.706593
0.082136 0.809772 0.793836 0.725309 0.694095 0.64898 0.626978 0.599473 0.59547 0.581397 0.573513 ... 0.586311 0.592736 0.597789 0.604167 0.616226 0.622785 0.631491 0.642134 0.653861 0.663374
0.101300 0.787272 0.729749 0.699616 0.660475 0.639075 0.630131 0.607787 0.585332 0.61016 0.592824 ... 0.602833 0.6077 0.613387 0.632459 0.629226 0.632978 0.640612 0.652157 0.656678 0.667324
0.139630 0.700404 0.676649 0.633906 0.64033 0.627129 0.612061 0.604403 0.608253 0.595995 0.597314 ... 0.603544 0.606819 0.611619 0.615782 0.620716 0.626328 0.63185 0.638215 0.645109 0.652771
0.235455 0.67782 0.65927 0.646185 0.648583 0.630159 0.623844 0.620389 0.627533 0.624577 0.618819 ... 0.624261 0.626626 0.629493 0.631696 0.635163 0.638742 0.641585 0.645732 0.649011 0.654019
0.312115 0.645647 0.637526 0.635954 0.606658 0.611758 0.607161 0.604857 0.603153 0.609707 0.60298 ... 0.607241 0.609691 0.611057 0.612252 0.615549 0.618212 0.621182 0.624025 0.626991 0.62996
0.386037 0.631536 0.637275 0.624029 0.609374 0.604428 0.601427 0.5933 0.60344 0.598088 0.598101 ... 0.60103 0.602816 0.605744 0.606735 0.608069 0.610551 0.613187 0.615241 0.617832 0.620523
0.465435 0.62765 0.624497 0.617369 0.61475 0.609964 0.60793 0.607493 0.605615 0.598597 0.603988 ... 0.606175 0.60996 0.612616 0.612968 0.612792 0.615498 0.615216 0.619268 0.621151 0.623507
0.561259 0.618947 0.618689 0.604615 0.600456 0.603497 0.594773 0.593847 0.592181 0.592129 0.592798 ... 0.595098 0.596076 0.596728 0.598658 0.599016 0.600639 0.602159 0.603942 0.605598 0.60749
0.714579 0.607155 0.609017 0.603926 0.593225 0.595732 0.59908 0.596881 0.596262 0.596125 0.596259 ... 0.597759 0.596051 0.597003 0.597996 0.599117 0.607239 0.604934 0.606078 0.606581 0.60494
0.810404 0.605527 0.60152 0.59781 0.600697 0.598748 0.597421 0.589942 0.589297 0.589467 0.589545 ... 0.590852 0.592566 0.59661 0.597432 0.591462 0.598762 0.595916 0.595952 0.595015 0.596688

12 rows × 22 columns

Nappe et smile de volatilité en 3D puis en 2D¶

Ce code permet de visualiser la surface de volatilité implicite pour les options CALL de Tesla ($\text{TSLA}$) à l'aide de Plotly.

In [26]:
# Nettoyage des données
vol_surface = vol_surface.apply(pd.to_numeric, errors='coerce').fillna(0)

# Création des grilles
maturities_grid, strikes_grid = np.meshgrid(vol_surface.index, vol_surface.columns)
volatility_grid = np.array(vol_surface).T

# Suppression des valeurs aberrantes
threshold = volatility_grid.mean() + 2 * volatility_grid.std()
volatility_grid = np.clip(volatility_grid, a_min=0, a_max=threshold)

# Rééchantillonnage pour réduire le bruit
points = np.array([strikes_grid.flatten(), maturities_grid.flatten()]).T
values = volatility_grid.flatten()
new_strikes = np.linspace(strikes_grid.min(), strikes_grid.max(), 50)
new_maturities = np.linspace(maturities_grid.min(), maturities_grid.max(), 50)
new_strikes_grid, new_maturities_grid = np.meshgrid(new_strikes, new_maturities)
volatility_grid_resampled = griddata(points, values, (new_strikes_grid, new_maturities_grid), method='cubic')

# Lissage avec filtre Gaussien
volatility_grid_smooth = gaussian_filter(volatility_grid_resampled, sigma=6)

# --- Graphique Mesh3D ---
fig = go.Figure(data=[go.Mesh3d(
    x=new_strikes_grid.flatten(),
    y=new_maturities_grid.flatten(),
    z=volatility_grid_smooth.flatten(),
    intensity=volatility_grid_smooth.flatten(),  # Intensité basée sur la volatilité
    colorscale="Viridis",  # Palette de couleurs
    opacity=0.8  # Transparence
)])

# Mise à jour des axes, du layout et des dimensions
fig.update_layout(
    title="Nappe de Volatilité Impliquée (Mesh3D - Lissée)",
    scene=dict(
        xaxis_title="Strike Price",
        yaxis_title="Time to Maturity (Years)",
        zaxis_title="Implied Volatility",
    ),
    template="plotly_white",
    width=800,  
    height=800  
)

# Affichage du graphique
pio.show(fig)
In [27]:
maturity_index = new_maturities_grid[:, 0].argmin()  # Choisir une maturité au milieu de la grille
smile_strikes = new_strikes_grid[maturity_index, :]  # Les strikes correspondants
smile_volatility = volatility_grid_smooth[maturity_index, :]  # Volatilité pour cette maturité

# Tracé du smile
plt.figure(figsize=(10, 6))
plt.plot(smile_strikes, smile_volatility, marker='o', linestyle='-', color='b', label='Smile de Volatilité')

# Configuration du graphique
plt.title("Smile de Volatilité Implicite")
plt.xlabel("Strike Price")
plt.ylabel("Implied Volatility")
plt.grid(True, linestyle='--', alpha=0.5)  # Grille légère
plt.legend()
plt.show()
No description has been provided for this image

Interprétations :

  1. Variation de la volatilité en fonction du strike :

    • On observe que la volatilité implicite est plus faible pour des prix d'exercice intermédiaires (autour de 350 à 400).
    • Aux extrémités (strikes élevés ou faibles), la volatilité implicite augmente significativement. Ce phénomène est souvent lié à la structure de volatilité souriante ou convexe (« volatility smile ») fréquemment observée sur les marchés d'options.
  2. Variation de la volatilité avec la maturité :

    • Pour des échéances courtes (proches de 0), la volatilité implicite reste relativement uniforme.
    • En revanche, pour des maturités plus longues (proches de 1 an), la volatilité semble s'accroître de manière importante, particulièrement aux extrémités des strikes.
  3. Structure globale de la surface :

    • Le creux au centre de la surface indique que les options proches du strike à la monnaie (at-the-money) présentent des volatilités plus faibles.
    • Les hausses marquées aux extrémités de la surface signalent une plus grande incertitude pour les options très "hors la monnaie" ou "dans la monnaie".
  4. Effet smile :

    • La volatilité implicite plus élevée aux extrémités traduit une plus grande incertitude sur les options dont le strike est très éloigné du prix actuel de l'action Tesla.
    • Cela reflète souvent la perception des investisseurs sur les mouvements extrêmes du prix sous-jacent.
  5. Augmentation avec la maturité :

    • La hausse de la volatilité implicite pour des maturités plus longues peut être interprétée comme une augmentation de l'incertitude à mesure que l'horizon temporel s'élargit.
  6. Utilité :

    • Cette surface de volatilité implicite est cruciale pour ajuster les modèles de valorisation des options, comme le modèle de Heston, et pour comprendre les attentes implicites du marché concernant la volatilité future.

Calibration du modèle de Heston sur les données¶

Chargement des données¶

Ce code collecte et structure les données des prix d'options d'achat (CALL) pour Tesla (symbole boursier TSLA) afin de générer une surface de volatilité ou une matrice des prix des options en fonction des maturités et des strikes.

In [12]:
ticker = 'TSLA'
stock = yf.Ticker(ticker)

# Retrieve all option expiration dates
expirations = stock.options

# Create a dictionary to store market prices
market_prices = {}

# Retrieve the CALL options for each expiration
for expiry in expirations:
    options = stock.option_chain(expiry)
    calls = options.calls
    market_prices[expiry] = {
        'strike': calls['strike'],
        'price': (calls['bid'] + calls['ask']) / 2
    }

# Get all available strikes
all_strikes = [v['strike'] for i, v in market_prices.items()]
common_strikes = set.intersection(*map(set, all_strikes))
common_strikes = sorted(common_strikes)

# Filter and organize the data.
prices = []
maturities = []
for date, v in market_prices.items():
    maturities.append((dt.strptime(date, '%Y-%m-%d') - dt.today()).days / 365.25)
    price = [v['price'][i] for i, x in enumerate(v['strike']) if x in common_strikes]
    prices.append(price)

# Convert to DataFrame
price_arr = np.array(prices, dtype=object)
np.shape(price_arr)
volSurface = pd.DataFrame(price_arr, index=maturities, columns=common_strikes)
volSurface = volSurface.iloc[(volSurface.index > 0.04) & (volSurface.index < 1), volSurface.columns <= 300]
volSurface
Out[12]:
145.0 150.0 165.0 175.0 200.0 210.0 220.0 230.0 240.0 250.0 260.0 270.0 280.0 290.0 300.0
0.043806 194.25 189.375 174.175 164.325 139.475 129.8 119.975 109.525 99.85 89.875 80.4 70.425 61.075 52.05 43.3
0.062971 194.475 189.35 174.4 164.675 139.9 130.075 120.225 110.4 100.6 91.225 80.975 71.6 62.25 53.925 45.2
0.082136 194.8 189.9 175.2 165.1 140.125 130.65 120.85 110.625 101.2 91.325 81.925 72.45 63.525 54.775 47.0
0.101300 194.875 190.225 175.35 165.45 140.6 130.925 120.975 111.35 101.55 92.125 82.675 73.675 65.225 56.8 48.725
0.139630 195.525 190.6 175.875 165.775 141.425 131.65 122.025 112.075 102.725 93.15 84.7 76.15 67.85 60.15 53.35
0.235455 196.65 191.775 176.95 167.45 143.3 134.05 124.225 115.9 107.075 98.6 90.95 82.8 75.45 68.6 62.725
0.312115 197.325 192.475 178.075 168.65 145.075 135.375 127.125 118.3 110.025 102.275 93.55 86.675 79.675 73.15 67.025
0.386037 198.3 193.925 179.325 169.9 146.725 137.8 129.3 120.825 113.4 105.4 97.525 90.425 83.75 77.05 71.95
0.465435 199.525 194.825 180.8 171.8 149.05 140.725 132.125 123.8 116.25 108.75 101.775 94.95 88.6 82.675 76.95
0.561259 200.525 195.9 182.15 173.175 151.25 142.6 134.75 127.0 119.875 112.15 105.275 99.2 92.525 86.725 81.15
0.714579 201.575 198.475 185.1 175.625 155.45 147.475 139.625 131.75 125.125 118.275 111.2 105.425 100.025 94.4 89.175
0.810404 204.75 199.575 186.475 177.975 156.825 149.0 142.2 134.85 128.0 121.4 115.6 109.65 104.0 97.975 92.85

Ce code transforme et enrichit les données de la surface de volatilité pour une utilisation plus flexible dans des calculs ou visualisations.

In [13]:
# Convertir le volSurface en format long pour chaque combinaison strike-maturity
volSurfaceLong = volSurface.melt(ignore_index=False).reset_index()
volSurfaceLong.columns = ['maturity', 'strike', 'price']

# Vérification explicite pour la colonne 'iv'
def get_iv(row):
    try:
        # Vérifiez si 'maturity' et 'strike' existent dans volSurface
        return volSurface.loc[row['maturity'], row['strike']]
    except KeyError:
        # Retournez NaN si la combinaison n'est pas trouvée
        return np.nan

# Applique la fonction corrigée
volSurfaceLong['iv'] = volSurfaceLong.apply(get_iv, axis=1)

# Calculer les taux sans risque pour chaque maturité
volSurfaceLong['rate'] = volSurfaceLong['maturity'].apply(lambda m: curve_fit(m))

Implémentation du programme de minimisation¶

Ce code effectue la calibration des paramètres du modèle de volatilité stochastique de Heston en minimisant l'erreur quadratique entre les prix d'options observés sur le marché et ceux calculés par le modèle.

In [14]:
# This is the calibration function
# heston_price(S0, K, v0, kappa, theta, sigma, rho, lambd, tau, r)
# Parameters are v0, kappa, theta, sigma, rho, lambd
# Define variables to be used in optimization
S0 = stock.history(period="1d")['Close'].iloc[-1]
r = volSurfaceLong['rate'].to_numpy('float')
K = volSurfaceLong['strike'].to_numpy('float')
tau = volSurfaceLong['maturity'].to_numpy('float')
P = volSurfaceLong['price'].to_numpy('float')
params = {
"v0": {"x0": 0.29, "lbub": [1e-4, 0.5]},
"kappa": {"x0": 1.6, "lbub": [1e-3, 10]},
"theta": {"x0": 0.11, "lbub": [1e-4, 0.5]},
"sigma": {"x0": 0.27, "lbub": [1e-2, 2]},
"rho": {"x0": 0.59, "lbub": [-1, 1]},
"lambd": {"x0": -0.9, "lbub": [-1, 1]},
}

x0 = [param["x0"] for key, param in params.items()]
bnds = [param["lbub"] for key, param in params.items()]
from scipy.optimize import minimize
# Fonction de minimisation de l'erreur quadratique
def SqErr(x):
    v0, kappa, theta, sigma, rho, lambd = x
    err = np.sum((P - heston_price_rec(S0, K, v0, kappa, theta, sigma, rho, lambd, tau, r))**2 / len(P))
    return err

La fonction SqErr(x) calcule l'erreur quadratique moyenne entre les prix d'options observés ($P$) et les prix modélisés par le modèle de Heston.

  1. Décompression des paramètres :
    Les paramètres passés à la fonction sont décompressés dans $v_0$, $\kappa$, $\theta$, $\sigma$, $\rho$, $\lambda$.

  2. Prix calculés par le modèle de Heston :
    La fonction heston_price_rec est appelée avec les paramètres calibrés et les données des options ($S_0$, $K$, $\tau$, $r$).
    Cette fonction renvoie les prix calculés des options basés sur le modèle de Heston.

  3. Erreur quadratique moyenne :
    L'erreur quadratique moyenne est calculée comme suit : $ \text{Erreur Quadratique} = \frac{\sum \left(P_{\text{observé}} - P_{\text{modèle}}\right)^2}{\text{Nombre total d’options}} $

  4. Retour de la fonction :
    Cette fonction renvoie la somme des erreurs normalisées par le nombre d'observations.

L'optimisation numérique¶

Après la définition théorique dans Python, le code suivant effectue une optimisation numérique pour calibrer les paramètres du modèle de Heston. Voici une explication détaillée :

In [15]:
result = minimize(SqErr, x0, tol = 1e-3, method='SLSQP', options={'maxiter': 1e4 }, bounds=bnds)
result
Out[15]:
 message: Optimization terminated successfully
 success: True
  status: 0
     fun: 0.6649186287894399
       x: [ 4.045e-01  1.645e+00  1.186e-02  3.893e-01  1.000e+00
           -1.000e+00]
     nit: 18
     jac: [ 6.641e-01 -5.157e-02  4.669e-01 -1.163e-01 -2.787e-01
           -5.493e-02]
    nfev: 134
    njev: 18

Ce code extrait les valeurs optimales des paramètres du modèle de Heston à partir du résultat de l'optimisation

In [16]:
v0, kappa, theta, sigma, rho, lambd = [param for param in result.x]
v0, kappa, theta, sigma, rho, lambd
Out[16]:
(np.float64(0.40453128613864486),
 np.float64(1.645126310203305),
 np.float64(0.011863275418814049),
 np.float64(0.3893346159811936),
 np.float64(0.9999999999999998),
 np.float64(-0.9999999999999998))

Calcul des prix et organisation en Dataframe¶

In [17]:
# Calcule les prix Heston et Black-Scholes
heston_prices = heston_price_rec(S0, volSurfaceLong['strike'], v0, kappa, theta, sigma, rho, lambd, volSurfaceLong['maturity'], r)

# Ajoute les colonnes des prix calculés au DataFrame
volSurfaceLong['heston_price'] = heston_prices

# Calcule l'erreur en pourcentage entre les prix du marché et les prix Heston
volSurfaceLong['error_percentage_hp'] = abs(volSurfaceLong['price'] - volSurfaceLong['heston_price']) / volSurfaceLong['price'] * 100

# Affiche le DataFrame avec les nouvelles colonnes
volSurfaceLong
Out[17]:
maturity strike price iv rate heston_price error_percentage_hp
0 0.043806 145.0 194.25 194.250 0.044497 193.851504 0.205146
1 0.062971 145.0 194.475 194.475 0.044453 194.194157 0.144411
2 0.082136 145.0 194.8 194.800 0.044409 194.536589 0.135221
3 0.101300 145.0 194.875 194.875 0.044366 194.878813 0.001957
4 0.139630 145.0 195.525 195.525 0.044282 195.562674 0.019268
... ... ... ... ... ... ... ...
175 0.386037 300.0 71.95 71.950 0.043787 72.566206 0.856437
176 0.465435 300.0 76.95 76.950 0.043644 77.204284 0.330454
177 0.561259 300.0 81.15 81.150 0.043481 82.271728 1.38229
178 0.714579 300.0 89.175 89.175 0.043242 89.461386 0.321151
179 0.810404 300.0 92.85 92.850 0.043105 93.505812 0.706313

180 rows × 7 columns

Par la suite, ce tableau est conçu pour analyser et synthétiser la performance du modèle de Heston en mesurant l'erreur en pourcentage entre les prix calculés (par le modèle) et les prix observés sur le marché, à l'aide de plusieurs métriques statistiques. Les options avec des prix très faibles ont tendance à avoir des erreurs relatives élevées car les petites différences entre le prix du marché et le prix calculé par le modèle peuvent apparaître significatives en termes de pourcentage. En filtrant par seuils croissants de prix, on analyse comment le modèle de Heston performe sur les options les plus significatives (avec des prix plus élevés), qui sont généralement moins sensibles aux petites erreurs relatives. Cela aide à comprendre si le modèle est plus fiable pour des options plus "importantes" (prix élevés) par rapport à celles qui sont plus sensibles aux fluctuations (prix faibles).

In [18]:
# Calcule les erreurs moyennes pour différents seuils de prix pour les erreurs Heston
mean_error_hp_0 = volSurfaceLong[volSurfaceLong['price'] > 0]['error_percentage_hp'].mean()
mean_error_hp_1 = volSurfaceLong[volSurfaceLong['price'] > 1]['error_percentage_hp'].mean()
mean_error_hp_3 = volSurfaceLong[volSurfaceLong['price'] > 3]['error_percentage_hp'].mean()
mean_error_hp_20 = volSurfaceLong[volSurfaceLong['price'] > 20]['error_percentage_hp'].mean()

# Crée un tableau récapitulatif avec les moyennes filtrées et la médiane pour le modèle Heston
error_metrics = pd.DataFrame({
    "Metric": ["Mean (Filtered for price > 0$)", "Mean (Filtered for price > 1$)",
               "Mean (Filtered for price > 3$)", "Mean (Filtered for price > 20$)", "Median Heston"],
    "Mean error Heston (%)": [f"{mean_error_hp_0:.2f}%", f"{mean_error_hp_1:.2f}%",
                              f"{mean_error_hp_3:.2f}%", f"{mean_error_hp_20:.2f}%",
                              f"{volSurfaceLong['error_percentage_hp'].median():.2f}%"]
})

# Affiche le tableau des erreurs moyennes en utilisant tabulate
print(tabulate(error_metrics, headers='keys', tablefmt='fancy_grid', showindex=False))
╒═════════════════════════════════╤═════════════════════════╕
│ Metric                          │ Mean error Heston (%)   │
╞═════════════════════════════════╪═════════════════════════╡
│ Mean (Filtered for price > 0$)  │ 0.49%                   │
├─────────────────────────────────┼─────────────────────────┤
│ Mean (Filtered for price > 1$)  │ 0.49%                   │
├─────────────────────────────────┼─────────────────────────┤
│ Mean (Filtered for price > 3$)  │ 0.49%                   │
├─────────────────────────────────┼─────────────────────────┤
│ Mean (Filtered for price > 20$) │ 0.49%                   │
├─────────────────────────────────┼─────────────────────────┤
│ Median Heston                   │ 0.40%                   │
╘═════════════════════════════════╧═════════════════════════╛

Dans ce tableau, l'erreur moyenne est 1.24% pour toutes les catégories filtrées. Cela indique que :

  • Le modèle de Heston est cohérent dans sa performance, quelle que soit la gamme de prix des options.
  • Le filtrage par seuils de prix (bas ou élevé) n'a pas d'impact significatif sur l'erreur moyenne.
  • Cette cohérence montre que le modèle de Heston s'applique uniformément bien sur toutes les options considérées dans l'échantillon, qu'elles soient faiblement ou fortement pricées.

Graphique afin de vérifier la calibration du modèle de Heston par rapport aux données du marché¶

Ce code réalise une visualisation 3D des prix des options du marché et des prix calculés à partir du modèle de Heston, permettant de comparer les deux ensembles de données de manière visuelle et d'évaluer la performance du modèle.

In [19]:
heston_prices = heston_price_rec(S0, K, v0, kappa, theta, sigma, rho, lambd, tau, r)
volSurfaceLong['heston_price'] = heston_prices
init_notebook_mode()
fig = go.Figure(data=[go.Mesh3d(x=volSurfaceLong.maturity, y=volSurfaceLong.strike,\
z=volSurfaceLong.price, color='limegreen', opacity=0.55)])
fig.add_scatter3d(x=volSurfaceLong.maturity, y=volSurfaceLong.strike,\
z=volSurfaceLong.heston_price, mode='markers',marker=dict(size=7))
fig.update_layout(
title_text='Market Prices (Mesh) vs Calibrated Heston Prices (Markers)',
scene = dict(xaxis_title='TIME (Years)',
yaxis_title='STRIKES (Pts)',
zaxis_title='INDEX OPTION PRICE (Pts)'),
height=800,
width=800)
fig.show(renderer='notebook')

Les points rouges (modèle de Heston) suivent globalement la surface verte (marché), ce qui indique que le modèle est globalement cohérent avec les prix du marché. Cela montre que le modèle de Heston, après calibration, est capable de capturer les dynamiques des prix des options sur le marché. Toutefois, on y observe quelques limites.\

En effet, Certaines régions montrent des écarts visibles entre les points rouges et la surface verte. Ces écarts pourraient indiquer :

  • Une imprécision du modèle de Heston dans ces plages de strikes ou de maturités.
  • Des imperfections dans la calibration des paramètres du modèle.
  • Des caractéristiques spécifiques du marché (comme des anomalies de volatilité implicite) que - le modèle ne capture pas.

Après avoir calibré le modèle de Heston sur les données des options américaines Tesla, il est pertinent de poursuivre l'analyse en explorant un modèle de volatilité locale. Bien que le modèle de Heston capture efficacement la dynamique stochastique de la volatilité et les interactions entre les variables sous-jacentes, il peut présenter des limites lorsqu'il s'agit de reproduire précisément les prix observés sur le marché pour chaque strike et maturité. En effet, les sourires et les déformations spécifiques de la surface de volatilité implicite, particulièrement visibles dans certaines régions de strike-maturité, peuvent nécessiter une approche plus flexible. Le modèle de volatilité locale, qui attribue une volatilité spécifique à chaque combinaison de strike et de maturité, constitue un complément idéal pour affiner la calibration et réduire les écarts résiduels. Cette transition vers un modèle de volatilité locale permettra d'approfondir la compréhension des comportements observés sur le marché et d'améliorer la précision des estimations des sensibilités (grecques) pour des applications pratiques telles que la gestion des portefeuilles et la tarification des options.

Calibration d'un modèle de volatilité locale¶

Motivations de la calibration d'un modèle de volatilité locale à la Dupire¶

1. Capturer les particularités locales de la volatilité

  • Les modèles comme Heston reposent sur l'hypothèse que la volatilité est stochastique mais partiellement homogène sur la structure par terme des prix d'options. Or, en réalité, la volatilité implique des comportements spécifiques à chaque strike et maturité.
  • Un modèle de volatilité locale permet de s'ajuster finement aux variations de volatilité implicite observées sur le marché pour chaque combinaison strike-maturité, en attribuant une volatilité différente à chaque point.

2. Réduire les erreurs de calibration

  • Bien que le modèle de Heston capture la dynamique de la volatilité et des prix d'options, il présente parfois des écarts significatifs par rapport aux prix du marché dans certaines régions spécifiques (comme observé dans le graphique).
  • La calibration d'un modèle de volatilité locale permet de minimiser ces erreurs, car il s'ajuste directement à la surface de volatilité implicite en tant qu'entrée.

3. Meilleure reproduction des smiles de volatilité

  • Le modèle de Heston capture généralement bien les sourires et sourcils (smiles et skews) de volatilité, mais il reste limité pour des structures plus complexes.
  • Un modèle de volatilité locale peut suivre précisément les smiles et corriger les asymétries observées dans la surface de volatilité implicite.

4. Requêtes spécifiques des acteurs du marché

  • Les acteurs du marché, comme les traders d'options ou les desks de structuration, recherchent souvent des modèles qui reproduisent parfaitement les prix d'options disponibles (calibration exacte). Un modèle de volatilité locale est souvent préféré dans ce cas, car il est capable de s'ajuster directement à la surface de volatilité implicite.

5. Complémentarité avec les modèles de volatilité stochastique

  • Les modèles de volatilité locale ne modélisent pas les dynamiques futures de la volatilité (c'est une faiblesse par rapport aux modèles de volatilité stochastique). Cependant, leur précision dans la calibration peut servir de base solide pour améliorer ou comparer les résultats des modèles de volatilité stochastique.
  • En pratique, on peut combiner les deux approches pour obtenir des résultats optimaux :
    • Volatilité locale pour une précision instantanée dans la calibration.
    • Volatilité stochastique pour simuler les évolutions futures.

6. Meilleure gestion des portefeuilles d'options

  • Dans la gestion des portefeuilles d'options, une estimation précise de la volatilité pour chaque strike et maturité est essentielle pour calculer les sensibilités (grecques) comme Delta, Vega, ou Gamma.
  • Un modèle de volatilité locale fournit une estimation plus précise des grecques que le modèle de Heston, particulièrement pour des structures complexes de volatilité implicite.

7. Limites du modèle de Heston

  • Le modèle de Heston, bien qu'élégant mathématiquement, peut échouer à reproduire certaines caractéristiques locales spécifiques du marché, comme :
    • Les régions avec une volatilité implicite très élevée ou très basse.
    • Des déformations spécifiques à certaines maturités ou strikes.
  • Le modèle de volatilité locale peut combler ces lacunes, car il repose sur une hypothèse beaucoup plus flexible.

Explication des étapes du code¶

Nous utilisons l'API Yahoo Finance pour récupérer les données d'options Tesla. Les dates d'expiration des options disponibles sont extraites dans la variable expirations.

Initialisation des listes après récupération des données¶

Initialisation de listes vides pour stocker les strikes, maturités, prix observés sur le marché, et prix calculés avec le modèle. $S_0$ représente le dernier prix de clôture de l'action Tesla.

Implémentation de la formule BS adaptée¶

Implémentation de la formule de Black-Scholes pour calculer le prix des options call. Ajout d'une gestion des cas où la maturité ou la volatilité est nulle pour éviter une division par zéro.

Boucle pour récupérer les données d'options et filtrer¶

Nous définissons une tolérance de 10% pour l'erreur relative entre les prix du modèle et les prix observés. Une boucle parcourt toutes les dates d'expiration pour récupérer les données des options call et calculer leur maturité.

Filtrage des strikes et interpolation¶

Filtrage des options avec une maturité comprise entre 0.04 et 1 an et des strikes inférieurs à 250. Interpolation cubique des volatilités implicites pour chaque strike afin de modéliser la volatilité locale.

Dans ce projet, nous avons utilisé une version modifiée du modèle de Black-Scholes pour intégrer une volatilité dépendante du strike $K$ et de la maturité $T$. Contrairement au modèle classique, où la volatilité est constante, cette approche ajuste la volatilité à partir des volatilités implicites observées sur le marché, obtenues par interpolation. La dynamique du modèle reste donnée par :

$ dS_t = r S_t dt + \sigma(K, T) S_t dW_t^\mathbb{Q} $

où $\sigma(K, T)$ est une fonction implicite des strikes et maturités, permettant de capturer les skews et smiles de volatilité. Ce modèle, bien que plus simple que celui de Dupire, est particulièrement efficace pour reproduire les déformations de volatilité observées sur les marchés, tout en restant dans le cadre de Black-Scholes.

Ce modèle peut être vu comme un approche locale de la volatilité de BS. En effet, ayant eu des soucis de calibrations avec Dupires cette approche a été adoptée.

Création des graphiques 3D de visualiation¶

Un graphique 3D est généré pour comparer les prix du marché (en rouge) avec ceux modélisés par la volatilité locale (surface en bleu/vert).

Création d'un dataframe récapitulatif¶

Un DataFrame est créé pour résumer les résultats (prix observés, prix modélisés, erreurs). Ce tableau permet une inspection détaillée des écarts entre le marché et le modèle.

Code python et interprétation¶

Code python avec des étapes expliquées auparavant.

In [34]:
# Récupération des données Tesla via Yahoo Finance
ticker = 'TSLA'
data = yf.Ticker(ticker)

# Obtenir les dates d'expiration des options disponibles
expirations = data.options  # Liste des dates d'expiration disponibles

# Initialisation des listes pour les strikes, maturités, volatilités et prix observés
all_strikes = []
all_maturities = []
all_market_prices = []
all_model_prices = []

S0 = data.history(period='1d')['Close'].iloc[-1]  # Dernier prix de l'action Tesla

# Fonction Black-Scholes modifiée pour éviter la division par zéro
def bs_call(T, K, F0, sigma):
    if T == 0 or sigma == 0:  # Pour éviter les divisions par zéro
        return max(F0 - K, 0)
    sigma_sqrt_T = sigma * np.sqrt(T)
    d1 = (np.log(F0 / K) + 0.5 * sigma**2 * T) / sigma_sqrt_T
    d2 = d1 - sigma_sqrt_T
    return F0 * norm.cdf(d1) - K * norm.cdf(d2)

# Définir le seuil de tolérance pour l'erreur relative entre le modèle et le marché (en pourcentage)
tolerance_threshold = 10  # 10% d'erreur maximale acceptable

# Boucle sur les expirations disponibles pour récupérer les données
for expiration in expirations:
    # Récupérer les prix des options pour chaque date d'expiration
    option_data = data.option_chain(expiration)
    calls = option_data.calls
    maturity_in_years = (pd.to_datetime(expiration) - pd.Timestamp.now()).days / 365  # Maturité en années
    
    # Filtrer les maturités et les strikes
    if 0.04 < maturity_in_years < 1:  # Filtrage des maturités entre 0.04 et 1 an
        # Récupérer les strikes, prix observés et volatilités implicites
        strikes = np.array(calls['strike'].values)
        market_prices = np.array(calls['lastPrice'].values)
        implied_vols = np.array(calls['impliedVolatility'].values)
        
        # Filtrer les strikes <= 250
        strikes_filtered = strikes[strikes <= 250]
        market_prices_filtered = market_prices[:len(strikes_filtered)]
        implied_vols_filtered = implied_vols[:len(strikes_filtered)]
        
        # Interpolation de la volatilité locale pour chaque strike
        S_sigma0_interp = interp1d(strikes_filtered, implied_vols_filtered, kind='cubic', fill_value="extrapolate")
        
        # Calcul des prix d'options avec le modèle de volatilité locale
        model_prices = []
        for strike in strikes_filtered:
            vol_local = S_sigma0_interp(strike)
            model_price = bs_call(maturity_in_years, strike, S0, vol_local)
            model_prices.append(model_price)
        
        # Ajouter les données dans les listes
        all_strikes.extend(strikes_filtered)
        all_maturities.extend([maturity_in_years] * len(strikes_filtered))  # Répéter la même maturité pour chaque strike
        all_market_prices.extend(market_prices_filtered)
        all_model_prices.extend(model_prices)

# Conversion des listes en arrays pour créer des graphes 3D
all_strikes = np.array(all_strikes)
all_maturities = np.array(all_maturities)
all_market_prices = np.array(all_market_prices)
all_model_prices = np.array(all_model_prices)

# Calcul de l'erreur relative entre le modèle et le marché
relative_errors = np.abs((all_market_prices - all_model_prices) / all_market_prices) * 100

# Filtrage des points avec une erreur relative supérieure à la tolérance
valid_indices = relative_errors <= tolerance_threshold
filtered_strikes = all_strikes[valid_indices]
filtered_maturities = all_maturities[valid_indices]
filtered_market_prices = all_market_prices[valid_indices]
filtered_model_prices = all_model_prices[valid_indices]

# Reshape les arrays pour créer une grille pour la surface
strike_grid, maturity_grid = np.meshgrid(np.unique(filtered_strikes), np.unique(filtered_maturities))
model_prices_grid = griddata((filtered_strikes, filtered_maturities), filtered_model_prices, (strike_grid, maturity_grid), method='cubic')

# Création du graphique interactif avec Plotly
market_trace = go.Scatter3d(
    x=filtered_strikes, 
    y=filtered_maturities, 
    z=filtered_market_prices, 
    mode='markers',
    marker=dict(size=5, color='red', opacity=0.8),
    name='Prix Marché'
)

model_surface = go.Surface(
    x=strike_grid,
    y=maturity_grid,
    z=model_prices_grid,
    colorscale='Viridis',
    opacity=0.7,
    name='Prix Modèle (Volatilité Locale)'
)

layout = go.Layout(
    title="Comparaison des prix d'options (Marché vs Modèle Volatilité Locale) après filtrage sur l'erreur",
    scene=dict(
        xaxis_title='Strike',
        yaxis_title='Maturité (années)',
        zaxis_title='Prix de l\'option'
    ),
    height=800,
    width=800
)

# Affichage du graphique
fig = go.Figure(data=[market_trace, model_surface], layout=layout)
pio.show(fig)

# Création du DataFrame avec les prix du marché, les prix estimés et l'erreur de mesure
df_comparison = pd.DataFrame({
    'Strike': filtered_strikes,
    'Maturité (années)': filtered_maturities,
    'Prix Marché': filtered_market_prices,
    'Prix Modèle (Volatilité Locale)': filtered_model_prices,
    'Erreur (%)': relative_errors[valid_indices]
})

# Affichage du DataFrame avec l'erreur de mesure
df_comparison
Out[34]:
Strike Maturité (années) Prix Marché Prix Modèle (Volatilité Locale) Erreur (%)
0 75.0 0.043836 265.30 263.763259 0.579247
1 80.0 0.043836 273.05 258.899823 5.182266
2 85.0 0.043836 233.45 253.780445 8.708694
3 90.0 0.043836 255.95 248.882795 2.761166
4 100.0 0.043836 252.97 238.883236 5.568551
... ... ... ... ... ...
394 230.0 0.810959 143.17 134.660392 5.943709
395 235.0 0.810959 141.07 131.255339 6.957299
396 240.0 0.810959 139.30 127.801189 8.254710
397 245.0 0.810959 137.32 124.522547 9.319439
398 250.0 0.810959 131.00 121.193379 7.485971

399 rows × 5 columns

Le modèle de volatilité locale semble bien s'ajuster aux données du marché, avec une erreur relative moyenne basse et une bonne correspondance visuelle entre les points de marché et la surface du modèle. Les erreurs pour chaque point sont relativement faibles (la majorité des erreurs sont inférieures à 5 %, et certaines sont inférieures à 1 %). Cela suggère que le modèle de volatilité locale ajuste efficacement les prix des options pour cette plage de strikes et de maturités. Les erreurs augmentent légèrement pour certains strikes élevés, ce qui peut indiquer que le modèle de volatilité locale a plus de mal à capturer certains comportements spécifiques des options out-of-the-money ou à longue maturité.

Par la suite, ce tableau est conçu pour analyser et synthétiser la performance du modèle de volatilité locale comme Heston en mesurant l'erreur en pourcentage entre les prix calculés (par le modèle) et les prix observés sur le marché, à l'aide de plusieurs métriques statistiques. Les options avec des prix très faibles ont tendance à avoir des erreurs relatives élevées car les petites différences entre le prix du marché et le prix calculé par le modèle peuvent apparaître significatives en termes de pourcentage. En filtrant par seuils croissants de prix, on analyse comment le modèle de Heston performe sur les options les plus significatives (avec des prix plus élevés), qui sont généralement moins sensibles aux petites erreurs relatives. Cela aide à comprendre si le modèle est plus fiable pour des options plus "importantes" (prix élevés) par rapport à celles qui sont plus sensibles aux fluctuations (prix faibles).

In [21]:
# Calcule les erreurs moyennes pour différents seuils de prix pour les erreurs du modèle de volatilité locale
mean_error_vl_0 = df_comparison[df_comparison['Prix Marché'] > 0]['Erreur (%)'].mean()
mean_error_vl_1 = df_comparison[df_comparison['Prix Marché'] > 1]['Erreur (%)'].mean()
mean_error_vl_3 = df_comparison[df_comparison['Prix Marché'] > 3]['Erreur (%)'].mean()
mean_error_vl_20 = df_comparison[df_comparison['Prix Marché'] > 20]['Erreur (%)'].mean()

# Crée un tableau récapitulatif avec les moyennes filtrées et la médiane pour le modèle de volatilité locale
error_metrics_vl = pd.DataFrame({
    "Metric": ["Mean (Filtered for price > 0$)", "Mean (Filtered for price > 1$)",
               "Mean (Filtered for price > 3$)", "Mean (Filtered for price > 20$)", "Median Volatility Local Dupires"],
    "Mean error Volatility Local Dupires (%)": [f"{mean_error_vl_0:.2f}%", f"{mean_error_vl_1:.2f}%",
                                        f"{mean_error_vl_3:.2f}%", f"{mean_error_vl_20:.2f}%",
                                        f"{df_comparison['Erreur (%)'].median():.2f}%"]
})

# Affiche le tableau des erreurs moyennes en utilisant tabulate
print(tabulate(error_metrics_vl, headers='keys', tablefmt='fancy_grid', showindex=False))
╒═════════════════════════════════╤═══════════════════════════════════════════╕
│ Metric                          │ Mean error Volatility Local Dupires (%)   │
╞═════════════════════════════════╪═══════════════════════════════════════════╡
│ Mean (Filtered for price > 0$)  │ 4.69%                                     │
├─────────────────────────────────┼───────────────────────────────────────────┤
│ Mean (Filtered for price > 1$)  │ 4.69%                                     │
├─────────────────────────────────┼───────────────────────────────────────────┤
│ Mean (Filtered for price > 3$)  │ 4.69%                                     │
├─────────────────────────────────┼───────────────────────────────────────────┤
│ Mean (Filtered for price > 20$) │ 4.69%                                     │
├─────────────────────────────────┼───────────────────────────────────────────┤
│ Median Volatility Local Dupires │ 4.99%                                     │
╘═════════════════════════════════╧═══════════════════════════════════════════╛

Comparaison entre Heston et Volatilité locale¶

  1. Erreurs Moyennes :

    • Le modèle de Heston présente des erreurs moyennes plus faibles que dans le modèle de volatilité locale.

    Cela indique que le modèle de Heston est plus précis dans la modélisation des prix d'options par rapport au modèle de Volatilité Locale, du moins sur cet ensemble de données.

  2. Filtrage des Prix :

    • Pour les deux modèles, les erreurs moyennes restent stables quelles que soient les tranches de prix utilisées pour le filtrage (prix > 0, > 1, > 3, > 20). Cela suggère que l'erreur n'est pas fortement influencée par les variations de prix dans ces plages.
  3. Robustesse des Modèles :

    • Le modèle de Heston semble mieux s'ajuster à la réalité des données observées, probablement grâce à sa dynamique stochastique qui prend en compte les comportements de marché de manière plus sophistiquée.
    • Le modèle de Volatilité Locale, bien qu'il soit très flexible et capable de s'adapter à la structure du sourire de volatilité, semble moins performant sur cet ensemble de données.

Le modèle de Heston surpasse le modèle de Volatilité Locale (Dupire) en termes d'erreurs moyennes et médianes. Cependant, le modèle de Volatilité Locale reste utile pour capturer les spécificités du sourire de volatilité. Une analyse complémentaire pourrait inclure des ajustements supplémentaires ou une extension du modèle de Volatilité Locale pour mieux rivaliser avec Heston.

Conclusion du projet¶

Dans ce projet, nous avons exploré différents modèles de pricing d'options en nous concentrant principalement sur le modèle de Heston et le modèle de Volatilité Locale (Dupire). Nous avons également calculé une nappe de volatilité, ou "volatility surface", à partir du modèle de Black-Scholes, en illustrant comment les smiles et les structures de volatilité évoluent en fonction des strikes et des maturités. Ces visualisations ont permis de mieux comprendre les déformations observées sur les marchés financiers.

Le modèle de Heston, avec sa dynamique stochastique pour la variance, s'est avéré particulièrement efficace pour capturer les comportements complexes du marché, notamment les effets de corrélation et les variations de la volatilité dans le temps. Ce modèle offre une représentation globale et robuste des prix d'options.

Le modèle de Volatilité Locale a quant à lui démontré sa capacité à s'adapter aux spécificités des smiles de volatilité et aux variations locales en fonction des strikes et des maturités. Bien qu'il repose sur une approche déterministe de la volatilité, il constitue un outil précieux pour une analyse fine des structures de volatilité.

Enseignements du projet¶

Complémentarité des Modèles : Le modèle de Heston, avec ses dynamiques stochastiques, est particulièrement adapté pour capturer les évolutions globales de la volatilité sur les marchés, tandis que le modèle de Volatilité Locale se distingue par son ajustement précis aux déformations locales des smiles.

Améliorations Futures : Une approche hybride combinant les forces des deux modèles pourrait être envisagée, permettant de bénéficier à la fois de la robustesse globale du modèle de Heston et de l'adaptabilité locale du modèle de Volatilité Locale.

Extensions : Les prochaines étapes pourraient inclure l'évaluation d'autres modèles avancés, tels que le modèle SABR ou Stochastic Local Volatility, pour comparer leurs performances sur les données étudiées. De plus, des analyses supplémentaires sur des périodes de marché plus longues ou des actifs différents pourraient enrichir les résultats.